/*	--- 使用声明式事务 --- 

【使用Spring】操作【JDBC】虽然方便，但是我们在前面讨论JDBC的时候，讲到过【JDBC事务】，如果要【在Spring中操作事务】，没必要【手写JDBC事务】，可以使用【Spring提供的高级接口】来操作【事务】。

【Spring】提供了【一个PlatformTransactionManager】来表示【事务管理器】，【所有的事务】都由【它】负责【管理】。

而【事务】由【TransactionStatus】表示。

如果【手写事务代码】，【使用try...catch】如下： */
TransactionStatus tx = null;

try {
	// 开启事务
	tx = txManager.getTransaction(new DefaultTransactionDefinition());

	// 相关JDBC操作
	jdbcTemplate.update("...");
	jdbcTemplate.update("...");

	// 提交事务
	txManager.commit(tx);
} catch (RuntimeException e) {
	// 回滚事务
	txManager.rollback(tx);
	throw e;
}

/*
【Spring】为啥要【抽象出PlatformTransactionManager和TransactionStatus】？

原因是【JavaEE】除了【提供JDBC事务】外，它还支持【分布式事务JTA】（Java Transaction API）。

【分布式事务】是指【多个数据源】（比如多个数据库，多个消息系统）要在【分布式环境】下【实现事务的时候】，应该【怎么实现】。

【分布式事务】实现起来【非常复杂】，简单地说就是通过【一个分布式事务管理器】实现【两阶段】提交，但【本身数据库事务】就【不快】，基于【数据库事务】实现的【分布式事务】就【慢得难以忍受】，所以【使用率不高】。

【Spring】为了【同时支持"JDBC和JTA"这2种事务模型】，就【抽象出 PlatformTransactionManager】。

因为【我们的代码】只需要【JDBC事务】

因此，【在AppConfig】中，需要【再定义一个PlatformTransactionManager对应的Bean】，【它的实际类型】是【DataSourceTransactionManager】： */
@Configuration 
@ComponentScan 
@PropertySource("jdbc.properties")
public class AppConfig {
	//...

	@Bean 
	PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}
}

/*
使用【编程的方式】使用【Spring事务】仍然【比较繁琐】，更好的方式是【通过声明式事务】来实现。

使用【声明式事务】非常简单，【除了在AppConfig】中, 追加【一个上述定义的PlatformTransactionManager】外，再加【一个@EnableTransactionManagement】就可以【启用声明式事务】： */
@Configuration 
@ComponentScan 
@EnableTransactionManagement // 启用"声明式事务"
@PropertySource("jdbc.properties") 
public class AppConfig {
	//...
}

/*
然后，对【需要"事务"支持的"方法"】，【加一个@Transactional注解】： */
@Component 
public class UserService {
	// 此public方法,自动具有【事务支持】
	@Transactional 
	public User register(String email, String password, String name) {
		//...
	}
}

/*
或者更简单一点，直接在【Bean的class处】加上，表示【所有public方法】都【具有"事务支持"】： */
@Component 
@Transactional 
public class UserService {
	//...
}


/*
【Spring】对【一个声明式事务的方法】，如何开启【事务支持】？

原理仍然是【AOP代理】，即通过【自动创建Bean的Proxy】实现： */
public class UserService$$EnhancerBySpringCGLIB extends UserService {

	UserService target = ...;
	PlatformTransactionManager txManager = ...;

	public User register(String email, String password, String name) {

		TransactionStatus tx = null;

		try {
			tx = txManager.getTransaction(new DefaultTransactionDefinition());
			target.register(email, password, name);
			txManager.commit(tx);
		} catch (RuntimeException e) {

			txManager.rollback(tx);
			throw e;
		}
	}
}


/*
注意：[声明了@EnableTransactionManagement]后，不必额外添加@EnableAspectJAutoProxy。


==============================================


#	回滚事务


默认情况下，如果【发生了RuntimeException】，【Spring的声明式事务】将【自动回滚】。

在【一个事务方法】中，如果【程序】判断【需要回滚事务】，只需【抛出RuntimeException】，例如： */
@Transactional 
public buyProducts(long productId, int num) {
	//...

	if (store < num) {
		// 库存不够, 购买失败
		throw new IllegalArgumentException("No enough products.");
		// 因为标注了@Transactional 声明式事务,自动回滚
	}

	//...
}


/*
如果要针对【Checked Exception回滚事务】，需要【在@Transactional注解中,写出来】： */
@Transactional(rollbackFor = { RuntimeException.class, IOException.class })
public buyProducts(long productId, int num) throws IOException {
	//...
}

/*
上述代码表示在【抛出 RuntimeException / IOException时】，【事务将回滚】。

为了简化代码，我们强烈建议【业务异常体系】从【RuntimeException】派生

这样就【不必声明 何特殊异常】, 即可让【Spring的声明式事务】正常工作： */
public class BusinessException extends RuntimeException {
	//... 
}

public class LoginException extends BusinessException {
	//...
}

public class PaymentException extends BusinessException {
	//...
}


/*===========================================================

#	事务边界

在【使用事务】的时候，明确【事务边界】非常重要。

对于【声明式事务】，例如，【下面的register()方法】： */
@Component 
public class UserService {
	@Transactional 
	public User register(String email, String password, String name) { // 事务开始
		
		//...
	} // 事务结束
}

/*
【它的事务边界】就是【addBonus()方法の开始和结束】。

在现实世界中，【问题】总是要【复杂一点点】。

【用户注册后，能自动获得100积分】，因此，【实际代码】如下： */
@Component
public class UserService {
	@Autowired 
	BonusService bonusService;

	@Transactional 
	public User register(String email, String password, String name) {
		// 插入用户记录
		User user = jdbcTemplate.insert("...");

		// 增加100积分
		bonusService.addBonus(user.id, 100);
	}
}


/*
现在问题来了：
调用方（比如RegisterController）调用 UserService.register()这个事务方法，它在内部又调用了BonusService.addBonus()这个事务方法，一共有几个事务？

如果addBonus()抛出了异常需要回滚事务，register()方法的事务是否也要回滚？

问题的复杂度是不是一下子提高了10倍？


=========================================================


#	事务传播


要解决【上面的Proxy嵌套rollback问题】，我们首先要【定义事务的传播模型】。

假设【用户注册的入口】是【RegisterController】，它本身【没有事务】，仅仅是【调用 UserService.register() 这个事务方法】： */
@Controller 
public class RegisterController {
	@Autowired 
	UserService userService;

	@PostMapping("/register") // 处理post请求
	public ModelAndView doRegister(HttpServletRequest req) {

		String email = req.getParameter("email");
		String password = req.getParameter("password");
		String name = req.getParameter("name");

		User user = userService.register(email, password, name);

		return ...;
	}
}

/*
因此，【UserService.register()这个事务方法】的【起始和结束】，就是【事务的范围】。

我们需要关心的问题是，在【UserService.register()这个事务方法】内，【调用BonusService.addBonus()方法】，【我们期待的事务行为】是什么： */
@Transactional
public User register(String email, String password, String name) {
// 事务已开启:
    User user = jdbcTemplate.insert("...");
    // ???:
    bonusService.addBonus(user.id, 100);
} // 事务结束



/*
对于【大多数业务】来说，我们期待【BonusService.addBonus()的调用，会和UserService.register()的调用】 融合在一起

【它的行为】应该如下：

UserService.register() 已经【开启了一个事务】，那么在【UserService.register()内部】调用 BonusServiceaddBonus() 时，BonusService.addBonus()方法 就没必要【再开启一个新事务】，【直接加入到 BonusService.addBonus() 的"事务"里】就好了。


其实就相当于：

1. serService.register() 先执行了一条INSERT语句：INSERT INTO users ...
2. BonusService.addBonus() 再执行一条INSERT语句：INSERT INTO bonus ...


--------------------------


因此，"Spring的声明式事务"为【事务传播】定义了【几个级别】，默认【传播级别】就是"REQUIRED"

REQUIRED 的意思是，如果【当前没有事务】，就【创建一个新事务】; 如果【当前有事务】，就【加入到当前事务中】执行。


------------

1. 我们观察 UserService.register()方法，它在 RegisterController 中执行，因为 RegisterController 没有【事务】，因此，UserService.register()方法 会【自动创建一个新事务】。

2. 在 UserService.register()方法内部，调用 BonusService.addBonus()方法时，因为 BonusService.addBonus() 检测到【当前已经有事务】了，因此，它会【加入到"当前事务"】中执行。

--------------------

因此，整个业务流程的事务边界就清晰了：它【只有一个事务】，并且【范围 就是 UserService.register()方法】。

有的童鞋会问：把 BonusService.addBonus()方法的@Transactional去掉，变成一个普通方法，那不就规避了【复杂的传播模型】吗？

去掉BonusService.addBonus()方法的@Transactional，会引来【另一个问题】，即【其他地方】如果调用【BonusService.addBonus()方法】，那就【没法保证事务】了。


例如，【规定用户登录时积分+5】： */
@Controller 
public class LoginController {
	@Autowired 
	BonusService bonusService;

	@PostMapping("/login")
	public ModelAndView doLogin(HttpServletRequest req) {
		User user = ...;
		bonusService.addBonus(user.id, 5);
	}
}

/*
可见，BonusService.addBonus()方法 [必须要有] @Transactional，否则，【登录后】积分就【无法添加】了。

【默认的事务传播级别】是【REQUIRED】，它满足【绝大部分的需求】。

------------------------------

还有【一些其他的传播级别】：

· SUPPORTS：表示 如果【有事务】，就【加入到当前事务】; 如果【没有】，那也【不开启事务执行】。 这种传播级别可用于【查询方法】，因为【SELECT语句】既可以在事务内执行，也可以不需要事务；

· MANDATORY：表示必须要存在当前事务并加入执行，否则将抛出异常。这种传播级别可用于【核心更新逻辑】，比如【用户余额变更】，它总是【被其他事务方法调用，不能直接由非事务方法调用】；

· REQUIRES_NEW：表示不管当前有没有事务，都【必须开启一个新的事务执行】。如果当前已经有事务，那么【当前事务会挂起】，等【新事务】完成后，再【恢复执行】；

· NOT_SUPPORTED：表示【不支持事务】，如果【当前有事务】，那么【当前事务】会挂起，等【这个方法】执行完成后，再【恢复执行】；

· NEVER：和NOT_SUPPORTED相比，它不但【不支持事务】，而且在【监测到当前有事务时，会抛出异常】拒绝执行；

· NESTED：表示如果【当前有事务】，则开启【一个嵌套级别事务】; 如果【当前没有事务】，则开启【一个新事务】。

-------------------

上面这么多种事务的传播级别，其实【默认的REQUIRED】已经满足【绝大部分需求】

SUPPORTS 和 REQUIRES_NEW 在【少数情况下】会用到，其他【基本不会用到】，因为把【事务】搞得越复杂，不仅【逻辑】跟着复杂，而且【速度】也会越慢。


-----------

定义【事务的传播级别】也是写在 @Transactional 注解里的：*/
@Transactional(propagation = Propagation.REQUIRES_NEW) 
public Product createProduct() {
	//...
}


/*
现在只剩最后一个问题了：
	Spring是[如何传播事务]的？


我们在[JDBC]中[使用事务]的时候，是这么个写法： */
Connection conn = openConnection();

try {
	// 关闭自动提交
	conn.setAutoCommit(false);

	// 执行多条SQL语句
	insert();
	update();
	delete();

	// 提交事务
	conn.commit();
} catch (SQLException e) {
	// 回滚事务
	conn.rollback();
} finally {
	conn.setAutoCommit(true);
	conn.close();
}


/*
Spring使用【声明式事务】，最终也是通过【执行JDBC事务】来【实现功能】的

那么，【一个事务方法】，如何获知【当前是否存在事务】？

答案是: 使用[ThreadLocal]。
(Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递"同一个对象")

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

----------------------

Spring总是把【JDBC相关的 Connection实例 和 TransactionStatus实例】 绑定到
	ThreadLocal<Connection>
	ThreadLocal<TransactionStatus> 


如果【一个事务方法】从 ThreadLocal 未取到【事务】，那么它会打开【一个新的JDBC连接，同时开启一个新的事务】; 
否则，它就直接使用从 ThreadLocal 获取的【JDBC连接 以及 TransactionStatus】


因此，【事务】能正确传播的前提是，【方法调用】是在【一个线程内】才行。

如果像下面这样写： */
@Transactional 
public User register(String email, String password, String name) {
	// BEGIN TX-A
	User user = jdbcTemplate.insert("...");

	new Thread( () -> {
		// BEGIN TX-B
		bonusService.addBonus(user.id, 100);
		// END TX-B
	}).start();
} // END TX-A


/*
在【另一个线程】中调用BonusService.addBonus()，它根本【获取不到"当前事务"】，因此，UserService.register() 和 BonusService.addBonus() 两个方法，将分别开启【2个完全独立的事务】。

换句话说，【事务】只能在【当前线程】传播，无法【跨"线程"传播】。


Q: 那如果我们想实现【跨线程传播】事务呢？

A: 原理很简单，就是要[想办法]把[当前线程]绑定到[ThreadLocal的 Connection和TransactionStatus实例]传递给[新线程]，但【实现起来】非常复杂，根据【异常】回滚更加复杂，不推荐自己去实现。



========================================================


#	练习

使用声明式事务  */




/*===========================================================


#	----- 使用声明式事务 の 小结 ----- 


1. 【Spring提供的"声明式事务"】极大地方便了: 在[数据库]中使用[事务]，正确使用[声明式事务]的关键在于 确定好[事务边界]，理解[事务传播级别]。



------------------


#	手动配置

在 xml 配置中的事务配置信息

<tx:annotation-driven />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
</bean>




------------

#	自动配置配置:

1. 在AppConfig中，需要再定义一个PlatformTransactionManager对应的Bean，它的实际类型是DataSourceTransactionManager：

2. 在AppConfig中追加一个上述定义的PlatformTransactionManager外，再加一个@EnableTransactionManagement就可以启用声明式事务：*/
@Configuration 
@ComponentScan 
@EnableTransactionManagement // 2.启用【声明式事务】
@ProprtySource("jdbc.properties") 
public class AppConfig {
	//... 

	@Bean // 1.定义一个@Bean PlatformTransactionManager 
	PlatformTransactionManager createTxManager(@Autowired DataSource dataSource) {
		return new DataSourceTransactionManager(dataSource);
	}
}


/*------------

使用 声明式事务

3. 对需要事务支持的方法，加一个@Transactional注解： */
@Component
public class UserService {
    
    @Transactional // 此public方法, 将自动具【有事务支持】
    public User register(String email, String password, String name) {
       ...
    }
}

/*
【事务支持】原理, 仍然是【AOP代理】，通过自动创建【Bean的Proxy】实现： 
(注意：
	声明了 @EnableTransactionManagement 后
	不必额外添加 @EnableAspectJAutoProxy
)
*/
public class UserService$$EnhancerBySpringCGLIB extends UserService {
  
    UserService target = ...;
    PlatformTransactionManager txManager = ...;

    public User register(String email, String password, String name) {
        TransactionStatus tx = null;
        try {
            tx = txManager.getTransaction(new DefaultTransactionDefinition());
            target.register(email, password, name);
            txManager.commit(tx);
        } catch (RuntimeException e) {
            txManager.rollback(tx);
            throw e;
        }
    }
    //...
}







